﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using Engine.Dtd.Schema;
using System.Diagnostics;

namespace Engine.Sgml.Parser {
    internal sealed class AttributeReader : IElementReader {
        private static char[] emptyValue = new char[0];
        private StringBuilder builder;

        public AttributeReader(StringBuilder builder) {
            Guard.ArgumentNotNull(builder, "builder");
            this.builder = builder;
            }

        public SElement Read(SgmlStreamReader reader, SElement parent, Context context) {
            while (CanRead(reader, context.TraceSource)) {
                SAttribute attribute = parent.Attributes.Push();
                attribute.Clear();
                attribute.LineNumber = reader.LineNumber;
                attribute.LinePosition = reader.LinePosition;

                string name = context.NameReader.ScanPrefixNameToken(reader);
                Debug.Assert(name != null, "Attribute name is null.", reader.Context());
                // Omit duplicate attributes
                if (!context.Behavior.PreserveDuplicateAttributes && parent.Attributes.Contains(name)) {
                    context.TraceSource.TraceEvent(TraceEventType.Warning, 0, "Ignore duplicate attribute '{0}' {1}.", name, attribute.Context());
                    parent.Attributes.Pop();
                    ReadAttributeValue(reader, context);
                    }
                else {
                    attribute.Reset(name);
                    attribute.Parent = parent;
                    attribute.NodeType = SgmlNodeType.Attribute;
                    attribute.Value = ReadAttributeValue(reader, context);
                    attribute.InitializeDeclaration(context.Schema, context.Behavior.CaseSensitiveTags);
                    }
                builder.Clear();
                }
            return null;
            }

        private bool CanRead(SgmlStreamReader stream, TraceSource trace) {
            stream.SkipWhitespace();
            do {
                switch (stream.CurrentChar) {
                    case Characters.ClosingTagMarkup:
                        char ch = stream.PeekChar();
                        switch (ch) {
                            case Characters.EndTagMarkup:
                                return false;
                            default:
                                if (XmlConvert.IsStartNCNameChar(ch)) {
                                    stream.ReadChar();
                                    return true;
                                    }
                                else
                                    trace.TraceEvent(TraceEventType.Error, 0,
                                        "The character '{0}', hexadecimal value 0x{1} is illegal at the beginning of an attribute name. Ln {2} Col {3}",
                                        ch, string.Format("{0:X}", Convert.ToInt32(ch)), stream.LineNumber, stream.LinePosition
                                        );
                                break;
                            }
                        break;
                    case Characters.EndTagMarkup:
                    case Characters.StartTagMarkup:
                    case Characters.EndOfFile:
                        return false;
                    default:
                        if (XmlConvert.IsStartNCNameChar(stream.CurrentChar))
                            return true;
                        else
                            trace.TraceEvent(TraceEventType.Error, 0,
                                            "The character '{0}', hexadecimal value 0x{1} is illegal at the beginning of an attribute name. Ln {2} Col {3}",
                                            stream.CurrentChar, string.Format("{0:X}", Convert.ToInt32(stream.CurrentChar)), stream.LineNumber, stream.LinePosition
                                            );
                        break;
                    }
                stream.ReadChar();
                } while (true);
            }

        private string ReadAttributeValue(SgmlStreamReader reader, Context context) {
            reader.SkipWhitespace();
            switch (reader.CurrentChar) {
                case Characters.EqualsChar:
                    reader.ReadChar();
                    reader.SkipWhitespace();
                    switch (reader.CurrentChar) {
                        case Characters.Apostrophe:
                            reader.ReadChar();
                            if (reader.CurrentChar == Characters.Apostrophe) {
                                reader.ReadChar();
                                return string.Empty;
                                }
                            else
                                return ReadLiteral(reader, Characters.Apostrophe, context);
                        case Characters.QuotationMark:
                            reader.ReadChar();
                            if (reader.CurrentChar == Characters.QuotationMark) {
                                reader.ReadChar();
                                return string.Empty;
                                }
                            else
                                return ReadLiteral(reader, Characters.QuotationMark, context);
                        case Characters.ClosingTagMarkup:
                            return ReadAttributeValueWithoutQuotationMark(reader, context);
                        case Characters.EndTagMarkup:
                            return string.Empty;
                        default:
                            return ReadAttributeValueWithoutQuotationMark(reader, context);
                        }
                default:
                    return string.Empty;
                }
            }

        private string ReadAttributeValueWithoutQuotationMark(SgmlStreamReader stream, Context context) {
            builder.Clear();
            do {
                switch (stream.CurrentChar) {
                    case Characters.StartEntityReference:
                        context.EntityResolver.Resolve(stream, builder);
                        break;
                    case Characters.ClosingTagMarkup:
                        if (stream.PeekChar() == Characters.EndTagMarkup) {
                            builder.Append(stream.CurrentChar);
                            return builder.ToString();
                            }
                        goto default;
                    case Characters.StartTagMarkup:
                        if (stream.PeekChar() == Characters.ClosingTagMarkup)
                            return builder.ToString();
                        goto default;
                    case Characters.Whitespace:
                    case Characters.EndTagMarkup:
                        return builder.ToString();
                    default:
                        builder.Append(stream.CurrentChar);
                        stream.ReadChar();
                        break;
                    }
                } while (stream.CurrentChar != Characters.EndOfFile);
            return builder.ToString();
            }

        private string ReadLiteral(SgmlStreamReader stream, char terminator, Context context) {
            context.SetLineInfo(stream);
            builder.Clear();
            do {
                if (stream.CurrentChar == Characters.StartEntityReference) {
                    context.EntityResolver.Resolve(stream, builder);
                    }
                else {
                    builder.Append(stream.CurrentChar);
                    stream.ReadChar();
                    }
                } while (!stream.EndOfStream && terminator != stream.CurrentChar);

            if (stream.EndOfStream)
                context.TraceSource.TraceEvent(TraceEventType.Error, 0, "The value of attribute was not closed. Ln {0} Col {1}", context.LineNumber, context.LinePosition);
            else if (stream.CurrentChar == terminator)
                stream.ReadChar();
            return builder.ToString();
            }
        }
    }
